#!/usr/bin/env ruby
#
#     kanabuche.rb - A Ruby script to help learning hiragana and katakana
#     Japanese characters.
#
#  Copyright (c) 2007, Alex Marandon
#  All rights reserved.
#
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions are met:
#  
#    * Redistributions of source code must retain the above copyright notice,
#      this list of conditions and the following disclaimer.
#    * Redistributions in binary form must reproduce the above copyright notice,
#      this list of conditions and the following disclaimer in the documentation
#      and/or other materials provided with the distribution.
#    * Neither the name of kanabuche nor the names of its contributors
#      may be used to endorse or promote products derived from this software 
#      without specific prior written permission.
#  
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
#  POSSIBILITY OF SUCH DAMAGE.
# 
# TODO:
# - handle unproper inputs
# - add an optional limit number of questions
#

KANABUCHE_VERSION=0.1

require 'optparse'
require 'csv'
require 'ostruct'

module QuizList
  def length
    @list.length
  end

  def finished?
    return @iter_index == @list.length
  end

  def randomize!
    @list = @list.sort_by { rand }
  end

  def next
    return nil if @iter_index == @list.length
    yield @type, @answer_type, @list[@iter_index]
    @iter_index += 1
  end
end

class NumbersBase
  include QuizList
  attr_accessor :list

  NUMBERS = { "一" => "1",
               "二" => "2",
               "三" => "3",
               "四" => "4",
               "五" => "5",
               "六" => "6",
               "七" => "7",
               "八" => "8",
               "九" => "9",
               "十" => "10",
               "百" => "100",
               "千" => "1000",
               "万" => "10000" }
  def initialize
    @type = 'Kanji'
    @answer_type = 'Digits'
    @iter_index = 0
    @list = NUMBERS.collect do |elem| 
      struct = OpenStruct.new
      struct.jp_char = elem[0]
      struct.west_equiv = elem[1]
      struct 
    end
  end
end

HIRAGANA_CSV=<<EOF
  , a   , i , u   , e , o
  , あ  , い, う  , え, お
k , か  , き, く  , け, こ
s , さ  , し(shi), す  , せ, そ
t , た  , ち(chi), つ(tsu)  , て, と
n , な  , に, ぬ  , ね, の
h , は  , ひ, ふ(fu), へ, ほ
m , ま  , み, む  , め, も
y , や  ,   , ゆ  ,   , よ
r , ら  , り, る  , れ, ろ
w , わ  ,   , を(o)  ,   , ん(n)
g , が  , ぎ, ぐ  , げ, ご
z , ざ  , じ(ji), ず(zu)  , ぜ, ぞ
d , だ  , ぢ(ji), づ(zu)  , で, ど
b , ば  , び, ぶ  , べ, ぼ
p , ぱ  , ぴ, ぷ  , ぺ, ぽ
ky, きゃ,   , きゅ,   , きょ
sh, しゃ,   , しゅ,   , しょ
ch, ちゃ,   , ちゅ,   , ちょ
ny, にゃ,   , にゅ,   , にょ
hy, ひゃ,   , ひゅ,   , ひょ
my, みゃ,   , みゅ,   , みょ
ry, りゃ,   , りゅ,   , りょ
gy, ぎゃ,   , ぎゅ,   , ぎょ
j,  じゃ,   , じゅ,   , じょ
by, びゃ,   , びゅ,   , びょ
py, ぴゃ,   , ぴゅ,   , ぴょ
x , ぁ  , ぃ, ぅ  ,ぇ , ぉ
EOF

KATAKANA_CSV=<<EOF
  , a   , i , u   , e , o
  , ア  , イ, ウ  , エ, オ
k , カ  , キ, ク  , ケ, コ
s , サ  , シ(shi), ス , セ, ソ
t , タ  , チ(chi), ツ(tsu) , テ, ト
n , ナ  , ニ, ヌ  , ネ, ノ
h , ハ  , ヒ, フ(fu), ヘ, ホ
m , マ  , ミ, ム  , メ, モ
y , ヤ  ,   , ユ  ,   , ヨ
r , ラ  , リ, ル  , レ, ロ
w , ワ  ,   , ヲ(o)  ,   , ン(n)
g , ガ  , ギ, グ  , ゲ, ゴ
z , ザ  , ジ(ji), ズ(zu)  , ゼ, ゾ
d , ダ  , ヂ(ji), ヅ(zu)  , デ, ド
b , バ  , ビ, ブ  , ベ, ボ
p , パ  , ピ, プ  , ペ, ポ
ky, キャ,   , キュ,   , キョ
sh, シャ,   , シュ,   , ショ
ch, チャ,   , チュ,   , チョ
ny, ニャ,   , ニュ,   , ニョ
hy, ヒャ,   , ヒュ,   , ヒョ
my, ミャ,   , ミュ,   , ミョ
ry, リャ,   , リュ,   , リョ
gy, ギャ,   , ギュ,   , ギョ
j,  ジャ,   , ジュ,   , ジョ
by, ビャ,   , ビュ,   , ビョ
py, ピャ,   , ピュ,   , ピョ
EOF

class Kana
  attr_accessor :west_equiv, :jp_char, :consonant, :vowel
  def initialize(jp_char, consonant, vowel)
    if jp_char =~ /(.+)\((.+)\)/
      @jp_char = $1
      @west_equiv = $2
    else
      @jp_char = jp_char
      @west_equiv = consonant + vowel
    end
    @consonant = consonant
    @vowel = vowel
  end
end

class KanaBase
  include QuizList
  attr_accessor :list
  def initialize(type)
    @type = type
    @answer_type = 'Romaji'
    @iter_index = 0
    lines = []
    data = (type == "Hiragana") ? HIRAGANA_CSV : KATAKANA_CSV
    CSV.parse(data) do |line| 
      line.each { |elem| elem.strip! } 
      lines.push(line)
    end
    @vowel_list = lines.shift
    @vowel_list.shift
    @list = []
    lines.each do |line|
      consonant = line.shift
      i = 0
      line.each do |kana_char|
        if kana_char != ""
          # some possible combinations of consonants and vowel don't
          # exist in Japanese, so we can encounter empty strings here
          vowel = @vowel_list[i]
          kana = Kana.new(kana_char, consonant, vowel)
          @list.push(kana)
        end
        i += 1
      end
    end
  end
  def filter!(criteria_list)
    @list.delete_if { |kana|
      not (criteria_list.include?(kana.vowel) or 
           criteria_list.include?(kana.consonant))
    }
  end
  def kana(romaji)
    result = @list.find { |kana| kana.west_equiv == romaji }
    if result == nil
      result
    else
      result.jp_char
    end
  end
  def romaji(kana_char)
    result = @list.find { |kana| kana.jp_char == kana_char }
    if result == nil
      result
    else
      result.west_equiv
    end
  end
  def kana_word_to_romaji(word)
    result = ""
    word.split("").each do |c|
      result << romaji(c)
    end
    return result
  end
  def each 
    @list.each { |kana| yield kana }
  end
end

class Exercise
    attr_reader :success_nb, :length, :start_time
    def initialize(bases, work_list, input_method, output_method, 
                   print_method = proc {})
        @bases = bases
        @input_method = input_method
        @output_method = output_method
        @print_method = print_method
        for base in @bases
            if work_list.length > 0 and base.respond_to?(:filter!)
                base.filter!(work_list)
            end
            base.randomize!
        end
        @success_nb = 0
        @length = @bases.inject(0) {|a,v| a += v.length}
        @start_time = Time.now
    end

    def run
        @bases.length.times do
            while @bases[rand(@bases.length)].next { |type, answer_type, kana| 
              ask(type, answer_type, kana) }
            end
            @bases.each {|b| @bases.delete(b)  if b.finished?  }
        end
    end

    def ask(type, answer_type, kana)
        @output_method.call  "\n#{ sprintf "%8s", type }: " + kana.jp_char
        @print_method.call(sprintf("%8s", answer_type) + ": ")
        answer = @input_method.call
        answer.strip!
        @output_method.call
        if kana.west_equiv == answer
            @success_nb += 1
            @output_method.call "Good!"
        else
            @output_method.call "No, #{kana.jp_char} is #{kana.west_equiv}."
        end
    end

    def printResults
      puts "\n"
      puts "#{@success_nb} good answers on #{@length} questions."
      printDuration
      puts "\n"
    end

    def printDuration
        duration = Time.now - @start_time
        minutes = (duration / 60).floor
        seconds = (duration % 60).floor
        if duration.floor > 0
            duration_str = "Duration: "
            duration_str += "#{minutes} minutes and " if minutes > 0
            duration_str += "#{seconds} seconds" if seconds > 0
            puts duration_str + "."
        end
    end
end

if __FILE__ == $0
  def buildSWorkListString(work_list)
      if work_list.include?("")
          work_list[work_list.index("")] = "vowels"
      end

      worklist_s = "all characters"
      if work_list.length > 0
          if work_list.length > 1
              worklist_s = work_list[0, work_list.length - 1].join(", ")
              worklist_s << " and " << work_list.last
          else
              worklist_s = work_list[0]
          end
      end
      return worklist_s
  end


  @bases = []

  opts = OptionParser.new
  opts.banner =<<EOF
Usage: #{$0} [options] [selection]

[selection] is a space-separated list of consonants and vowels that you want to
practice.  The first row of characters does not contain a consonant, it may be
requested by the empty string ("").  If no selection is provided, the program
will ask you about every characters. 
EOF
  opts.separator ""
  opts.separator "Options:"
  opts.on("-H", "--hiragana", "Practice hiragana") { |val| 
    @bases << KanaBase.new("Hiragana") 
  }
  opts.on("-K", "--katakana", "Practice katakana") { |val| 
    @bases << KanaBase.new("Katakana") 
  }
  opts.on("-N", "--numbers", "Practice numbers") { |val| 
    @bases << NumbersBase.new 
  }
  opts.on("-h", "--help", "Show this message") do
    puts opts
    exit
  end
  opts.on_tail("-v", "--version", "Show version") do
    puts KANABUCHE_VERSION
    exit
  end
  rest = opts.parse(*ARGV)

  if @bases.length == 0
    %w[hiragana katakana].each { |kana_type|
      @bases << KanaBase.new(kana_type.capitalize)
    }
  end

  exercise = Exercise.new(@bases, rest.clone, STDIN.method(:gets), 
                          method(:puts), method(:print))

  puts "Welcome to Kanabuche!\n\n"
  puts "Working on #{buildSWorkListString(rest)}. #{exercise.length} questions.\n\n"
  Signal.trap("INT") do 
    puts "\nInterrupted.\n"
    exercise.printResults
    exit -1
  end
  exercise.run
  exercise.printResults
end

#EOF
